/*
 *  Copyright (c) 2009 Ondrej Dusek
 *  All rights reserved.
 * 
 *  Redistribution and use in source and binary forms, with or without modification, 
 *  are permitted provided that the following conditions are met:
 *  Redistributions of source code must retain the above copyright notice, this list 
 *  of conditions and the following disclaimer.
 *  Redistributions in binary form must reproduce the above copyright notice, this 
 *  list of conditions and the following disclaimer in the documentation and/or other 
 *  materials provided with the distribution.
 *  Neither the name of Ondrej Dusek nor the names of their contributors may be
 *  used to endorse or promote products derived from this software without specific 
 *  prior written permission.
 * 
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
 *  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
 *  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
 *  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
 *  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
 *  BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
 *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
 *  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 
 *  OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 
 *  OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package en_deep.mlprocess;

import en_deep.mlprocess.exception.TaskException;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Hashtable;
import java.util.Vector;

/**
 * A general task to be computed or performed.
 * 
 * @author Ondrej Dusek
 */
public abstract class Task implements Serializable {

    /* DATA */

    /** The unique ID of the {@link Task} (for {@link TaskException}s and {@link TaskDescription.TaskStatus} progress update) */
    protected String id;

    /** The class parameters for this {@link Task} */
    protected Hashtable<String, String> parameters;

    /** The list of input files for this {@link Task} */
    protected Vector<String> input;

    /** The list of output files generated by this {@link Task} */
    protected Vector<String> output;

    /* METHODS */

    /**
     * A constructor to be used with the derived classes. Just sets the needed values. It performs cloning of all the
     * structures so that the originals are not harmed by changes performed by the task itself.
     * 
     * @param id the unique id of the task
     * @param parameters the parameters of this algorithm
     * @param input list of input files (paths relative to working directory)
     * @param output list of output files (paths relative to working directory)
     */
    protected Task(String id, Hashtable<String, String> parameters, Vector<String> input, Vector<String> output){
        this.id = id;
        this.parameters = (Hashtable<String, String>) parameters.clone();
        this.input = (Vector<String>) input.clone();
        this.output = (Vector<String>) output.clone();
    }

    /**
     * Performs the given task.
     */
    public abstract void perform() throws TaskException;

    /**
     * This creates a {@link Task} object of the specified class for the given
     * {@link TaskDescription}.
     *
     * TODO possibly add default package for classes ?
     *
     * @param desc the description of the class, containing all the necessary parameters
     * @return the {@link Task} object that may be processed by the {@link Worker}s
     * @throws TaskException if the task processing class was not found or has not the right parameters
     */
    static Task createTask(TaskDescription desc) throws TaskException {

        Task res = null;
        Class taskClass = null;
        Constructor taskConstructor = null;

        // retrieve the task class
        try {
            taskClass = Class.forName(desc.getAlgorithm());
        }
        catch (ClassNotFoundException ex) {
            throw new TaskException(TaskException.ERR_TASK_CLASS_NOT_FOUND, desc.getId());
        }

        // try to call a constructor with the given parameters
        try {
            taskConstructor = taskClass.getConstructor(String.class, Hashtable.class, Vector.class, Vector.class);
            res = (Task) taskConstructor.newInstance(desc.getId(), desc.getParameters(),
                    desc.getInput(),desc.getOutput());
        }
        catch(InvocationTargetException ex){

            if (ex.getCause().getClass().equals(TaskException.class)){
                throw (TaskException) ex.getCause();
            }
            Logger.getInstance().logStackTrace(ex.getCause(), Logger.V_DEBUG);
            throw new TaskException(TaskException.ERR_TASK_INIT_ERR, desc.getId(), ex.getMessage());
        }
        catch(Exception ex){
            Logger.getInstance().logStackTrace(ex, Logger.V_DEBUG);
            throw new TaskException(TaskException.ERR_TASK_CLASS_INCORRECT, desc.getId(), ex.getMessage());
        }

        return res;
    }


    /**
     * Returns the ID of this {@link Task}
     * @return the Task ID
     */
    public String getId(){
        return this.id;
    }

    /**
     * This returns the part of ID that resulted from task expansions, with '#'s replaced by '_'s.
     * @return the part of the task ID after '#', or "" if not applicable
     */
    public String getExpandedPartOfId(){
        String expPart = this.id.indexOf('#') == -1 ? "" : this.id.substring(this.id.indexOf('#'));
        if (expPart.startsWith("#")) {
            expPart = expPart.substring(1);
        }
        expPart = expPart.replace('#', '_');
        return expPart;
    }

    /**
     * Returns a boolean value of a class {@link #parameters parameter}, which is false if the parameter
     * value is "0" or "false" and true otherwise
     *
     * @param paramName the name of the parameter to be examined
     * @return the boolean value of the parameter
     */
    public boolean getBooleanParameterVal(String paramName) {

        if (this.parameters.get(paramName) != null){
            return (this.parameters.get(paramName).equals("0")
                    || this.parameters.get(paramName).equalsIgnoreCase("false"))
                    ? false : true;
        }
        else {
            return false;
        }
    }

    /**
     * This returns the numeric value of a parameter. It returns null if the value is null,
     * it throws an exception if the value is set and is not numeric.
     * @param paramName the name of the parameter
     * @return the numeric value of the parameter
     * @throws TaskException if the value is set, but not numeric
     */
    public Double getDoubleParameterVal(String paramName) throws TaskException {

        if (this.parameters.get(paramName) != null){
            try {
                return Double.parseDouble(this.parameters.get(paramName));
            }
            catch (NumberFormatException e){
                throw new TaskException(TaskException.ERR_INVALID_PARAMS, this.id, "Value of "
                        + paramName + " must be numeric.");
            }
        }
        else {
            return null;
        }
    }


    /**
     * This returns the integer value of a parameter. It returns null if the value is null,
     * 1 if the value is set but empty, it throws an exception if the value is set and is
     * not numeric.
     * 
     * @param paramName the name of the parameter
     * @return the numeric value of the parameter
     * @throws TaskException if the value is set, but not numeric
     */
    public Integer getIntParameterVal(String paramName) throws TaskException {

        if (this.parameters.get(paramName) != null){

            if (this.parameters.get(paramName).equals("")){ // set and empty -> return 1
                return 1;
            }
            try {
                return Integer.parseInt(this.parameters.get(paramName));
            }
            catch (NumberFormatException e){
                throw new TaskException(TaskException.ERR_INVALID_PARAMS, this.id, "Value of "
                        + paramName + " must be numeric.");
            }
        }
        else { // not set -> return null
            return null;
        }
    }

    /**
     * Returns a string value of a class {@link #parameters parameter}.
     * 
     * @param paramName the name of the desired parameter
     * @return the value of the given parameter, or null
     */
    public String getParameterVal(String paramName) {
        return this.parameters.get(paramName);
    }

    /**
     * Returns true, if the parameter with the given name is set.
     * @param paramName the name of the parameter to be examined
     * @return true, if the parameter is set
     */
    public boolean hasParameter(String paramName){
        return this.parameters.containsKey(paramName);
    }

    /**
     * This eliminates the posibility that the given Vector contains patterns by throwing an exception.
     * @param whereFrom the Vector to be tested
     * @throws TaskException if the given Vector contains patterns
     */
    protected void eliminatePatterns(Vector<String> whereFrom) throws TaskException {
        for (String str : whereFrom) {
            if (str.contains("*")) {
                throw new TaskException(TaskException.ERR_PATTERN_SPECS, this.id, "Patterns in I/O specs.");
            }
        }
    }

    /**
     * This checks if the given parameter is set. If not, an exception is thrown.
     * @param paramName the name of the required parameter
     * @throws TaskException it the parameter is not set
     */
    protected void requireParameter(String paramName) throws TaskException {
        if (!this.hasParameter(paramName)){
            throw new TaskException(TaskException.ERR_INVALID_PARAMS, this.id, "Missing required parameter: " + paramName);
        }
    }

}
